﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Synchronization.SimpleProviders;
using Microsoft.Synchronization;
using Microsoft.Synchronization.MetadataStorage;
using LitS3;
using System.IO;
using System.Globalization;

namespace Natol.MSSyncFxProviders.AmazonS3
{
    public class AWSS3SyncProvider : FullEnumerationSimpleSyncProvider//, Microsoft.Synchronization.SimpleProviders.ISimpleSyncProviderIdGenerator
    {
        string SyncName { get; set; }

        private const string SYNCFILENAME_PREFIX = "syncreplicaid-";
        private const string METADATAFILENAME_PREFIX = "syncmetadata-";

        // Use SqlMetadataStore to track version information and other metadata for each item
        // in the item store.
        SqlMetadataStore _metadataStore = null;
        private string _name = null;
        private string _replicaMetadataFile = null;
        private string _replicaIdFile = null;

        // The unique identifier for the replica that the provider represents.
        SyncId _replicaId = null;

        // The format for the IDs that represent each replica and item.
        SyncIdFormatGroup _idFormats = null;

        S3Service service { get; set; }
        S3BucketLoginData LoginData { get; set; }

        DirectoryInfo WorkingFolder { get; set; }

        public AWSS3SyncProvider(string syncName, S3BucketLoginData S3AccessDetails)
        {
            SyncName = syncName;

            LoginData = S3AccessDetails;
            service = LoginData.GetService("~");

            //set up working folder
            string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().Replace("-", String.Empty));
            Directory.CreateDirectory(tempPath);
            WorkingFolder = new DirectoryInfo(tempPath);

            //set id format
            // Set ReplicaIdFormat to use a GUID as an ID, and ItemIdFormat to use a GUID plus
            // an 8-byte prefix.
            _idFormats = new SyncIdFormatGroup();
            _idFormats.ItemIdFormat.IsVariableLength = false;
            _idFormats.ItemIdFormat.Length = 24;
            _idFormats.ReplicaIdFormat.IsVariableLength = false;
            _idFormats.ReplicaIdFormat.Length = 16;
            _idFormats.ChangeUnitIdFormat.Length = 4;
            _idFormats.ChangeUnitIdFormat.IsVariableLength = false;
        }

        private void CloseMetadataStore()
        {
            //close object
            try
            {
                if (_metadataStore!=null)
                    _metadataStore.Dispose();
            }
            finally
            {
                _metadataStore = null;
            }

            if (File.Exists(metadataPath))
            {
                //persist store to s3
                service.AddObject(metadataPath, LoginData.BucketName, metadataFileName);
            }
        }
        string metadataFileName
        {
            get
            {
                return String.Concat(METADATAFILENAME_PREFIX, SyncName);
            }
        }
        string metadataPath
        {
            get
            {
                return Path.Combine(WorkingFolder.FullName, metadataFileName);
            }
        }
        private void InitializeMetadataStore()
        {
           // SyncId id = ReplicaId;

            //download if it exists
            if (service.ObjectExists(LoginData.BucketName, metadataFileName))
            {
                service.GetObject(LoginData.BucketName, metadataFileName, metadataPath);
                _metadataStore = SqlMetadataStore.OpenStore(metadataPath);
            }
            else
            {
                _metadataStore = SqlMetadataStore.CreateStore(metadataPath);
            }

            
        }

        public SyncId ReplicaId
        {
            get
            {
                if (_replicaId == null)
                {
                    string syncFileName = String.Concat(SYNCFILENAME_PREFIX, SyncName);
                    
                    //get replica id from store
                    //if exists, read - otherwise create and write
                    if (service.ObjectExists(LoginData.BucketName, syncFileName))
                        _replicaId = new SyncId(new Guid(service.GetObjectString(LoginData.BucketName, syncFileName)));
                    else
                    {
                        SyncId replicaId = new SyncId(Guid.NewGuid());
                        service.AddObjectString(replicaId.GetGuidId().ToString(), LoginData.BucketName, syncFileName);
                        _replicaId = replicaId;
                    }

                }
                return _replicaId;
            }
        }

        /// <summary>
        /// Retreives metadata about each item in the store
        /// </summary>
        /// <param name="context"></param>
        public override void EnumerateItems(FullEnumerationContext context)
        {
            List<ItemFieldDictionary> items = new List<ItemFieldDictionary>();
            
            service.ForEachObject(LoginData.BucketName, entry =>
                {
                    if (!entry.Name.StartsWith(METADATAFILENAME_PREFIX) && !entry.Name.StartsWith(SYNCFILENAME_PREFIX))
                    {
                        bool addThis = true;

                        ItemFieldDictionary itemFields = new ItemFieldDictionary();
                        itemFields.Add(new ItemField(CUSTOM_FIELD_ID, typeof(ulong),(ulong)entry.Name.GetHashCode()));
                        itemFields.Add(new ItemField(CUSTOM_FIELD_NAME, typeof(string), entry.Name));
                        
                        if (entry is ObjectEntry)
                        {
                            var file = entry as ObjectEntry;
                            if (file.Size == 0)
                                addThis = false;
                            itemFields.Add(new ItemField(CUSTOM_FIELD_TIMESTAMP, typeof(ulong), (ulong)file.LastModified.ToBinary()));
                            itemFields.Add(new ItemField(CUSTOM_FIELD_SIZE, typeof(ulong), (ulong)file.Size));
                        }
                        if (addThis)
                            items.Add(itemFields);
                    }
                });

            context.ReportItems(items);
        }

         
        /// <summary>
        /// Anything we want to achieve at the beginning of each sync session
        /// </summary>
        public override void BeginSession()
        {
            
        }

        /// <summary>
        /// Instructs the provider to delete an object from the store
        /// </summary>
        /// <param name="keyAndExpectedVersion">Metadata associated with item</param>
        /// <param name="recoverableErrorReportingContext"></param>
        /// <param name="commitKnowledgeAfterThisItem"></param>
        public override void DeleteItem(ItemFieldDictionary keyAndExpectedVersion, RecoverableErrorReportingContext recoverableErrorReportingContext, out bool commitKnowledgeAfterThisItem)
        {
            //throw new NotImplementedException();
            commitKnowledgeAfterThisItem = false;
        }

        /// <summary>
        /// Anything we want to achieve at the end of each sync session. Usually closes handles on the metadata repository.
        /// </summary>
        public override void EndSession()
        {
            CloseMetadataStore();
        }

        /// <summary>
        /// Fetches an initialised MetadataStore. In most cases this is a SqlMetadataStore, stored as a file.
        /// </summary>
        /// <param name="replicaId"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public override MetadataStore GetMetadataStore(out Microsoft.Synchronization.SyncId replicaId, out System.Globalization.CultureInfo culture)
        {
            InitializeMetadataStore();
            replicaId = ReplicaId;
            culture = CultureInfo.CurrentCulture;
            return _metadataStore;
        }
        
        /// <summary>
        /// Where the actual data for objects is retreived in order to copy them to destinations.
        /// Since this provider concerns Files, and is intended to be compatible with the FileSyncProvider, we must return an instance of IFileDataRetreiver.
        /// </summary>
        /// <param name="keyAndExpectedVersion">Metadata of object to retreive data for</param>
        /// <param name="changeUnitsToLoad"></param>
        /// <param name="recoverableErrorReportingContext"></param>
        /// <returns></returns>
        public override object LoadChangeData(ItemFieldDictionary keyAndExpectedVersion, IEnumerable<Microsoft.Synchronization.SyncId> changeUnitsToLoad, RecoverableErrorReportingContext recoverableErrorReportingContext)
        {
            //to ensure compatibility with filesyncprovider, we must return IFileDataRetriever, which S3FileDataRetriever implements
            
            //Load file metadata from ItemFieldDictionary (which we assume was created on enumeration by this class)
            //and pass to S3FileDataRetriever

            string objectKey = keyAndExpectedVersion[CUSTOM_FIELD_NAME].Value as string;
            DateTime lastModified = DateTime.FromBinary(Convert.ToInt64(keyAndExpectedVersion[CUSTOM_FIELD_TIMESTAMP].Value));
            long objectSize = Convert.ToInt64(keyAndExpectedVersion[CUSTOM_FIELD_SIZE].Value);

            return new AWSS3FileDataRetriever(LoginData, objectKey, lastModified, objectSize);
        }

        #region SyncProviderSchemaInfo
        // The name of the metadata store custom column that is used to save a timestamp for
        // the last change on an item. This is used for change detection.
        //const string TIMESTAMP_COLUMNNAME = "timestamp";

        // The version of the provider (not the version of Sync Framework).
        // Provider version can be used to ensure that two different versions of a provider
        // can work with each other. For example, if you release version 2 of a provider, but
        // no fundamtental changes were made to data and metadata storage, handling, or transfer,
        // the new provider could retain a provider version of 1, which means the two providers
        // can interoperate.
        const short PROVIDER_VERSION = 1;

        // The integer value for each column in the item store. These values are used when
        // creating custom field definitions and identity rules for the ItemMetadataSchema.
        public const uint CUSTOM_FIELD_ID = 1;
        public const uint CUSTOM_FIELD_NAME = 2;
        public const uint CUSTOM_FIELD_TIMESTAMP = 3;
        public const uint CUSTOM_FIELD_SIZE = 4;

        /// <summary>
        /// Defines our Metadata Schema, and how it arranges ID, Name, Timestamp and Size of each object.
        /// </summary>
        public override ItemMetadataSchema MetadataSchema
        {
            get
            {
                CustomFieldDefinition[] customFields = new CustomFieldDefinition[4];
                customFields[0] = new CustomFieldDefinition(CUSTOM_FIELD_ID, typeof(ulong));
                customFields[1] = new CustomFieldDefinition(CUSTOM_FIELD_NAME, typeof(string),1024);
                customFields[2] = new CustomFieldDefinition(CUSTOM_FIELD_TIMESTAMP, typeof(ulong));
                customFields[3] = new CustomFieldDefinition(CUSTOM_FIELD_SIZE, typeof(ulong));

                IdentityRule[] identityRule = new IdentityRule[1];
                identityRule[0] = new IdentityRule(new uint[] { CUSTOM_FIELD_ID });

                return new ItemMetadataSchema(customFields, identityRule);

            }
        }

        /// <summary>
        /// Defines our ID schema for each object
        /// </summary>
        public override Microsoft.Synchronization.SyncIdFormatGroup IdFormats
        {
            get
            {
                return _idFormats;
            }
        }
        #endregion SyncProviderSchemaInfo

        #region SyncProvider Not Supported

        /// <summary>
        /// Adds a new object to the repository when it is a destination.
        /// Since this provider is concerned with files, the data will contain both the key and the data content for the file.
        /// </summary>
        /// <param name="itemData">Data for file</param>
        /// <param name="changeUnitsToCreate"></param>
        /// <param name="recoverableErrorReportingContext"></param>
        /// <param name="keyAndUpdatedVersion">Metadata of new Item</param>
        /// <param name="commitKnowledgeAfterThisItem"></param>
        public override void InsertItem(object itemData, IEnumerable<Microsoft.Synchronization.SyncId> changeUnitsToCreate, RecoverableErrorReportingContext recoverableErrorReportingContext, out ItemFieldDictionary keyAndUpdatedVersion, out bool commitKnowledgeAfterThisItem)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Updates an existing object in the repository when it is a destination.
        /// Since this provider is concerned with files, the data will contain both the key and the data content for the file.
        /// </summary>
        /// <param name="itemData">Data for file</param>
        /// <param name="changeUnitsToUpdate"></param>
        /// <param name="keyAndExpectedVersion">Existing Metadata</param>
        /// <param name="recoverableErrorReportingContext"></param>
        /// <param name="keyAndUpdatedVersion">New Metadata</param>
        /// <param name="commitKnowledgeAfterThisItem"></param>
        public override void UpdateItem(object itemData, IEnumerable<Microsoft.Synchronization.SyncId> changeUnitsToUpdate, ItemFieldDictionary keyAndExpectedVersion, RecoverableErrorReportingContext recoverableErrorReportingContext, out ItemFieldDictionary keyAndUpdatedVersion, out bool commitKnowledgeAfterThisItem)
        {
            throw new NotImplementedException();
        }
       
        #endregion SyncProvider Not Supported

        #region ISimpleSyncProviderIdGenerator Members

   
        #endregion

        /// <summary>
        /// Returns a basic Version Identification for this synchronisation provider, so that in the future we may make considerations for synchronising between different future versions of this code.
        /// </summary>
        public override short ProviderVersion
        {
            get { return PROVIDER_VERSION; }
        }
    }
}
